Skip to content

Add support for custom games (shard 2): custom game YAML and code generators - #2

Open
dejabot wants to merge 1 commit into
custom-games-shard-1from
custom-games-shard-2
Open

Add support for custom games (shard 2): custom game YAML and code generators#2
dejabot wants to merge 1 commit into
custom-games-shard-1from
custom-games-shard-2

Conversation

@dejabot

@dejabot dejabot commented Jun 30, 2026

Copy link
Copy Markdown

Adds support for custom games. Builds off of custom-games-shard-1 (shard 1).

This shard adds support for defining a custom game in a YAML configuration file, parsing that file, and generating code and tests based on the parsed configuration.

We use templates for generating both code and web source files. Note that web codegen is in this shard, but to balance shard sizes, the web templates are in a subsequent shard. The generator builds and its unit tests pass on their own; a full go generate needs shard 4's template sources.

@dejabot

dejabot commented Jun 30, 2026

Copy link
Copy Markdown
Author

Example Generated Code

To make the codegen concrete, here's the actual output of go generate ./... using the game defined in `examples/high_seas_havoc.yaml":

Input: game/examples/high_seas_havoc.yaml (abridged)
scoring_groups:
  - { id: ship, display_name: "Ship" }
  - { id: lair, display_name: "Lair" }
scoring_counts:
  - { id: hull,        scoring_group: ship, phases: [auto: 4,  teleop: 2] }
  - { id: deck,        scoring_group: ship, phases: [auto: 10, teleop: 5] }
  - { id: kraken_lair, scoring_group: lair, phases: [endgame: 10] }
statuses: [ leave (bool, auto), muster (enum none/partial/full, auto), park (bool, endgame) ]
ranking_tiebreakers: [ total_points, auto_points, lair ]
playoff_tiebreakers: [ lair, ship, auto_points ]

generated_score.go — raw scoring state (from score.go.tmpl)

One counter per (count × phase), [3] per-robot status arrays, a typed enum for muster, plus an Adjust…Count method per count:

type MusterStatus int
const ( MusterNone MusterStatus = iota; MusterPartial; MusterFull )

type Score struct {
	AutoHullCount, TeleopHullCount   int
	AutoDeckCount, TeleopDeckCount   int
	EndgameKrakenLairCount           int
	LeaveStatuses  [3]bool
	MusterStatuses [3]MusterStatus
	ParkStatuses   [3]bool
	Fouls     []Foul
	PlayoffDq bool
	Hub       Hub
}

func (s *Score) AdjustHullCount(phase Phase, delta int) bool {
	switch phase {
	case PhaseAuto:
		newVal := s.AutoHullCount + delta
		if newVal < 0 { newVal = 0 }
		if newVal == s.AutoHullCount { return false }
		s.AutoHullCount = newVal
		return true
	case PhaseTeleop:
		// …same shape for TeleopHullCount
	}
	return false
}

A string-keyed AdjustCount(id, phase, delta) dispatcher (also generated) routes the websocket's runtime id to these typed methods.

generated_score_summary.go — computed totals + group rollup (from score_summary.go.tmpl)

This is where scoring_group becomes real: hull and deck both land in ShipPoints; kraken_lair in LairPoints.

type ScoreSummary struct {
	ShipPoints  int   // hull + deck, across phases
	LairPoints  int   // kraken_lair
	LeavePoints, MusterPoints, ParkPoints           int
	AutoPoints, TeleopPoints, EndgamePoints, MatchPoints int
	FoulPoints, Score                               int
	AutonRpRankingPoint, ScoringRpRankingPoint, EndgameRpRankingPoint bool
	BonusRankingPoints                              int
	// …PlayoffDq, NumOpponentMajorFouls
}

Summarize sends each count's points to its group field while also accumulating phase totals:

autoHull := score.AutoHullCount * HullAutoPointsVal
autoPoints      += autoHull
summary.ShipPoints += autoHull
// …teleopHull, autoDeck, teleopDeck all += into summary.ShipPoints …
endgameKrakenLair := score.EndgameKrakenLairCount * KrakenLairEndgamePointsVal
endgamePoints      += endgameKrakenLair
summary.LairPoints += endgameKrakenLair

Each ranking-point bonus calls its hand-written logic and tallies bonus RP:

summary.AutonRpRankingPoint = ComputeAutonRP(*score, *opponentScore, *summary)
if summary.AutonRpRankingPoint { summary.BonusRankingPoints++ }

And DetermineMatchStatus becomes the playoff cascade (playoff_tiebreakers: [lair, ship, auto_points], opponent major fouls implicit first):

if s := comparePoints(red.NumOpponentMajorFouls, blue.NumOpponentMajorFouls); s != TieMatch { return s, "TIEBREAK: MAJOR FOULS" }
if s := comparePoints(red.LairPoints, blue.LairPoints); s != TieMatch { return s, "TIEBREAK: LAIR" }
if s := comparePoints(red.ShipPoints, blue.ShipPoints); s != TieMatch { return s, "TIEBREAK: SHIP" }
if s := comparePoints(red.AutoPoints, blue.AutoPoints); s != TieMatch { return s, "TIEBREAK: AUTO POINTS" }
return TieMatch, "TRUE TIE"

generated_ranking_fields.go — season ranking sort (from ranking_fields.go.tmpl)

ranking_tiebreakers: [total_points, auto_points, lair] → struct fields + a Played-normalized cascade:

type RankingFields struct {
	RankingPoints, MatchPoints, AutoPoints, LairPoints int
	Random                                            float64
	Wins, Losses, Ties, Disqualifications, Played     int
}

func (rankings Rankings) Less(i, j int) bool {
	a, b := rankings[i], rankings[j]
	if a.RankingPoints*b.Played != b.RankingPoints*a.Played { return a.RankingPoints*b.Played > b.RankingPoints*a.Played }
	if a.MatchPoints*b.Played   != b.MatchPoints*a.Played   { return a.MatchPoints*b.Played   > b.MatchPoints*a.Played }
	if a.AutoPoints*b.Played    != b.AutoPoints*a.Played    { return a.AutoPoints*b.Played    > b.AutoPoints*a.Played }
	if a.LairPoints*b.Played    != b.LairPoints*a.Played    { return a.LairPoints*b.Played    > b.LairPoints*a.Played }
	return a.Random > b.Random
}

@dejabot
dejabot requested review from rcahoon and wolffg June 30, 2026 05:00
@dejabot dejabot changed the title add support for custom games (shard 2): custom game YAML and code generators Add support for custom games (shard 2): custom game YAML and code generators Jun 30, 2026
@dejabot
dejabot force-pushed the custom-games-shard-2 branch from e10b967 to 4450a85 Compare July 14, 2026 08:41
@dejabot
dejabot force-pushed the custom-games-shard-1 branch from 134561c to 0f8df20 Compare July 14, 2026 08:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant